/*
 * The Unified Mapping Platform (JUMP) is an extensible, interactive GUI
 * for visualizing and manipulating spatial features with geometry and attributes.
 *
 * Copyright (C) 2003 Vivid Solutions
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * For more information, contact:
 *
 * Vivid Solutions
 * Suite #1A
 * 2328 Government Street
 * Victoria BC  V8T 5G5
 * Canada
 *
 * (250)385-6040
 * www.vividsolutions.com
 */
package com.vividsolutions.jump.workbench.ui.zoom;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.*;
import java.util.List;

import javax.swing.*;
import javax.swing.Timer;
import javax.swing.plaf.basic.BasicSliderUI;
import javax.swing.event.InternalFrameAdapter;
import javax.swing.event.InternalFrameEvent;

import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.Envelope;
import com.vividsolutions.jts.geom.Geometry;
import com.vividsolutions.jts.util.Assert;
import com.vividsolutions.jump.I18N;
import com.vividsolutions.jump.feature.Feature;
import com.vividsolutions.jump.feature.FeatureUtil;
import com.vividsolutions.jump.geom.EnvelopeUtil;
import com.vividsolutions.jump.geom.LineSegmentEnvelopeIntersector;
import com.vividsolutions.jump.util.Blackboard;
import com.vividsolutions.jump.util.CoordinateArrays;
import com.vividsolutions.jump.util.MathUtil;
import com.vividsolutions.jump.workbench.model.CategoryEvent;
import com.vividsolutions.jump.workbench.model.FeatureEvent;
import com.vividsolutions.jump.workbench.model.FeatureEventType;
import com.vividsolutions.jump.workbench.model.Layer;
import com.vividsolutions.jump.workbench.model.LayerEvent;
import com.vividsolutions.jump.workbench.model.LayerEventType;
import com.vividsolutions.jump.workbench.model.LayerListener;
import com.vividsolutions.jump.workbench.model.LayerManager;
import com.vividsolutions.jump.workbench.ui.GUIUtil;
import com.vividsolutions.jump.workbench.ui.LayerViewPanel;
import com.vividsolutions.jump.workbench.ui.LayerViewPanelContext;
import com.vividsolutions.jump.workbench.ui.LayerViewPanelProxy;
import com.vividsolutions.jump.workbench.ui.Viewport;
import com.vividsolutions.jump.workbench.ui.ViewportListener;
import com.vividsolutions.jump.workbench.ui.WorkbenchFrame;
import com.vividsolutions.jump.workbench.ui.plugin.scalebar.IncrementChooser;
import com.vividsolutions.jump.workbench.ui.plugin.scalebar.MetricSystem;
import com.vividsolutions.jump.workbench.ui.plugin.scalebar.RoundQuantity;
import com.vividsolutions.jump.workbench.ui.plugin.scalebar.ScaleBarRenderer;
import com.vividsolutions.jump.workbench.ui.renderer.java2D.Java2DConverter;

public class ZoomBar extends JPanel implements Java2DConverter.PointConverter {
    private int totalGeometries() {
        int totalGeometries = 0;
        // Restrict count to visible layers [mmichaud 2007-05-27]
        for (Iterator i = layerViewPanel().getLayerManager().getVisibleLayers(true).iterator(); i.hasNext();) {
            Layer layer = (Layer) i.next();
            totalGeometries += layer.getFeatureCollectionWrapper().size();
        }
        return totalGeometries;
    }

    private Envelope lastGoodEnvelope = null;
    private WorkbenchFrame frame;
    private BorderLayout borderLayout1 = new BorderLayout();
    private JSlider slider = new JSlider();
    private JLabel label = new JLabel();
    private IncrementChooser incrementChooser = new IncrementChooser();
    private Collection metricUnits = new MetricSystem(1).createUnits();
    
    // Add java2DConverter and affineTransform for coordinate decimation
    private Java2DConverter java2DConverter;
    private AffineTransform affineTransform;


    public ZoomBar(
	    boolean showingSliderLabels,
	    boolean showingRightSideLabel,
	    WorkbenchFrame frame)
	    throws NoninvertibleTransformException {
	    this.frame = frame;
	    this.showingSliderLabels = showingSliderLabels;
	    slider.addComponentListener(new ComponentAdapter() {
	        public void componentResized(ComponentEvent e) {
	            try {
	                updateComponents();
	            } catch (NoninvertibleTransformException x) {
	                //Eat it. [Jon Aquino]
	            }
	        }
	    });
	    if (showingSliderLabels) {
	        //Add a dummy label so that ZoomBars added to Toolboxes are
	        //packed properly. [Jon Aquino]
	        Hashtable labelTable = new Hashtable();
	        labelTable.put(new Integer(0), new JLabel(" "));
	        slider.setLabelTable(labelTable);
	    }
	    try {
	        jbInit();
	    } catch (Exception ex) {
	        ex.printStackTrace();
	    }
	    if (!showingRightSideLabel) {
	        remove(label);
	    }
	    label.addMouseListener(new MouseAdapter() {
	        public void mouseClicked(MouseEvent e) {
	            if (e.getClickCount() == 3 && SwingUtilities.isRightMouseButton(e)) {
	                viewBlackboard().put(USER_DEFINED_MIN_SCALE, null);
	                viewBlackboard().put(USER_DEFINED_MAX_SCALE, null);
	                clearModelCaches();
	            }
	        }
	    });
	    slider.addMouseMotionListener(new MouseMotionAdapter() {
	        //Use #mouseDragged rather than JSlider#stateChanged because we
	        //are interested in user-initiated slider changes, not programmatic
	        //slider changes. [Jon Aquino]
	        public void mouseDragged(MouseEvent e) {
	            try {
	                layerViewPanel().erase((Graphics2D) layerViewPanel().getGraphics());
	                drawWireframe();
	                ScaleBarRenderer scaleBarRenderer =
	                    (ScaleBarRenderer) layerViewPanel().getRenderingManager().getRenderer(
	                        ScaleBarRenderer.CONTENT_ID);
	                if (scaleBarRenderer != null) {
	                    scaleBarRenderer.paint(
	                        (Graphics2D) layerViewPanel().getGraphics(),
	                        getScale());
	                }
	                updateLabel();
	            } catch (NoninvertibleTransformException x) {
	                //Eat it. [Jon Aquino]
	            }
	        }
	    });
	    if (slider.getUI() instanceof BasicSliderUI) {
	        slider.addMouseMotionListener(new MouseMotionAdapter() {
	            public void mouseMoved(MouseEvent e) {
	                if (layerViewPanel() == dummyLayerViewPanel) {
	                    return;
	                }
	                try {
	                    slider.setToolTipText(
	                        I18N.get("ui.zoom.ZoomBar.zoom-to")+" "
	                            + chooseGoodIncrement(
	                                toScale(
	                                    ((BasicSliderUI) slider.getUI()).valueForXPosition(
	                                        e.getX())))
	                                .toString());
	                } catch (NoninvertibleTransformException x) {
	                    slider.setToolTipText(I18N.get("ui.zoom.ZoomBar.zoom"));
	                }
	            }
	        });
	    }
	    label.setPreferredSize(new Dimension(50, label.getHeight()));
	    slider.addKeyListener(new KeyAdapter() {
	        public void keyReleased(KeyEvent e) {
	            try {
	                if (e.getKeyCode() == KeyEvent.VK_LEFT
	                    || e.getKeyCode() == KeyEvent.VK_RIGHT) {
	                    gestureFinished();
	                }
	            } catch (NoninvertibleTransformException t) {
	                layerViewPanel().getContext().handleThrowable(t);
	            }
	        }
	    });
	    slider.addMouseListener(new MouseAdapter() {
	        public void mousePressed(MouseEvent e) {
	            if (!slider.isEnabled()) {
	                return;
	            }
	            layerViewPanel().getRenderingManager().setPaintingEnabled(false);
	        }
	        public void mouseReleased(MouseEvent e) {
	            try {
	                gestureFinished();
	            } catch (NoninvertibleTransformException t) {
	                layerViewPanel().getContext().handleThrowable(t);
	            }
	        }
	    });

        //
        // Whenever anything happens on an internal frame we want to do this.
        //
	    GUIUtil.addInternalFrameListener(
	            frame.getDesktopPane(),
	            GUIUtil.toInternalFrameListener(new ActionListener() {
	        public void actionPerformed(ActionEvent e) {
	            installListenersOnCurrentPanel();
	            try {
	                updateComponents();
	            } catch (NoninvertibleTransformException x) {
	                //Eat it. [Jon Aquino]
	            }
	        }
	    }));

        // added to use the decimator implemented in Java2DConverter [mmichaud 2007-05-27]
        java2DConverter = new Java2DConverter(this, 2);
        
	    installListenersOnCurrentPanel();
	    updateComponents();
	}


    private void installListenersOnCurrentPanel() {
        installViewListeners();
        installModelListeners();
    }

    private void installViewListeners() {
        
        //Use hash code to uniquely identify this zoom bar (there may be other
        //zoom bars) [Jon Aquino]
        String VIEW_LISTENERS_INSTALLED_KEY =
            Integer.toHexString(hashCode()) + " - VIEW LISTENERS INSTALLED";
        if (viewBlackboard().get(VIEW_LISTENERS_INSTALLED_KEY) != null) {
            return;
        }
        if ( layerViewPanel() == null ){
            return;
        }
        layerViewPanel().getViewport().addListener(new ViewportListener() {
            public void zoomChanged(Envelope modelEnvelope) {
                if (!viewBlackboard().get(CENTRE_LOCKED_KEY, false)) {
                    viewBlackboard().put(CENTRE_KEY, null);
                }
                viewBlackboard().put(SCALE_KEY, null);
                try {
                    if (layerViewPanel().getViewport().getScale() < getMinScale()) {
                        viewBlackboard().put(
                            USER_DEFINED_MIN_SCALE,
                            layerViewPanel().getViewport().getScale());
                    }
                    if (layerViewPanel().getViewport().getScale() > getMaxScale()) {
                        viewBlackboard().put(
                            USER_DEFINED_MAX_SCALE,
                            layerViewPanel().getViewport().getScale());
                    }
                    updateComponents();
                } catch (NoninvertibleTransformException e) {
                    //Eat it. [Jon Aquino]
                }
            }
        });
        viewBlackboard().put(VIEW_LISTENERS_INSTALLED_KEY, new Object());
    }

    private void installModelListeners() {

        //Use hash code to uniquely identify this zoom bar (there may be other
        //zoom bars) [Jon Aquino]
        String MODEL_LISTENERS_INSTALLED_KEY =
            Integer.toHexString(hashCode()) + " - MODEL LISTENERS INSTALLED";
        if (viewBlackboard().get(MODEL_LISTENERS_INSTALLED_KEY) != null) {
            return;
        }
        if ( layerViewPanel() == null ){
            return;
        }

        layerViewPanel().getLayerManager().addLayerListener(new LayerListener() {
            public void categoryChanged(CategoryEvent e) {}
            public void featuresChanged(FeatureEvent e) {
                if (e.getType() == FeatureEventType.ADDED
                    || e.getType() == FeatureEventType.DELETED
                    || e.getType() == FeatureEventType.GEOMETRY_MODIFIED) {
                    clearModelCaches();
                }
            }
            // add LayerEventType.VISIBILITY_CHANGED condition [mmichaud 2007-05-27]
            public void layerChanged(LayerEvent e) {
                if (e.getType() == LayerEventType.ADDED ||
                    e.getType() == LayerEventType.REMOVED ||
                    e.getType() == LayerEventType.VISIBILITY_CHANGED) {
                    clearModelCaches();
                }
            }
        });
        viewBlackboard().put(MODEL_LISTENERS_INSTALLED_KEY, new Object());
    }

    private void queueComponentUpdate() {
        componentUpdateTimer.restart();
    }
    /** Coalesces component updates */
    private Timer componentUpdateTimer =
        GUIUtil.createRestartableSingleEventTimer(200, new ActionListener() {
        public void actionPerformed(ActionEvent e) {
            try {
                updateComponents();
            } catch (NoninvertibleTransformException x) {
                //Eat it. [Jon Aquino]
            }
        }
    });

    public void updateComponents() throws NoninvertibleTransformException {
        LayerViewPanel layerViewPanel = layerViewPanel();
        if (layerViewPanel == dummyLayerViewPanel || layerViewPanel == null) {
            setComponentsEnabled(false);
            return;
        }
        setComponentsEnabled(true);
        //Must set slider value *before* updating the label on the right. [Jon Aquino]
        //I'm currently hiding the label on the right, to save real estate. [Jon Aquino]
        slider.setValue(
            toSliderValue(
                viewBlackboard().get(SCALE_KEY, layerViewPanel.getViewport().getScale())));
        updateLabel();
        updateSliderLabels();
    }

    private void gestureFinished() throws NoninvertibleTransformException {
        if (!slider.isEnabled()) {
            return;
        }
        try {
            viewBlackboard().put(CENTRE_LOCKED_KEY, true);
            try {
                layerViewPanel().getViewport().zoom(proposedModelEnvelope());
            } finally {
                viewBlackboard().put(CENTRE_LOCKED_KEY, false);
            }
        } finally {
            layerViewPanel().getRenderingManager().setPaintingEnabled(true);
        }
    }
    private Envelope proposedModelEnvelope() throws NoninvertibleTransformException {
        Coordinate centre =
            (Coordinate) viewBlackboard().get(
                CENTRE_KEY,
                EnvelopeUtil.centre(
                    layerViewPanel().getViewport().getEnvelopeInModelCoordinates()));
        double width = layerViewPanel().getWidth() / getScale();
        double height = layerViewPanel().getHeight() / getScale();
        Envelope proposedModelEnvelope =
            new Envelope(
                centre.x - (width / 2),
                centre.x + (width / 2),
                centre.y - (height / 2),
                centre.y + (height / 2));
        if (proposedModelEnvelope.getWidth() == 0 || proposedModelEnvelope.getHeight() == 0) {
            //We're zoomed waaay out! Avoid infinite scale. [Jon Aquino]
            proposedModelEnvelope = lastGoodEnvelope;
        } else {
            lastGoodEnvelope = proposedModelEnvelope;
        }
        return proposedModelEnvelope;
    }
    
   /**
    * Return the scale of the view according to the zoom bar
    */
    //getScale() public to implement Java2DConverter.PointConverter [mmichaud 2007-05-26]
    public double getScale() throws NoninvertibleTransformException {
        return toScale(slider.getValue());
    }
    
    private Stroke stroke = new BasicStroke(1);
    private void drawWireframe() throws NoninvertibleTransformException {
        Graphics2D g = (Graphics2D) layerViewPanel().getGraphics();
        g.setColor(Color.lightGray);
        g.setStroke(stroke);
        g.draw(getWireFrame());
    }
    private static final String SEGMENT_CACHE_KEY = ZoomBar.class.getName() + " - SEGMENT CACHE";
    private void clearModelCaches() {
        //Use LayerManager blackboard for segment cache, so that multiple
        //views can share it. [Jon Aquino]
        modelBlackboard().put(SEGMENT_CACHE_KEY, null);
        modelBlackboard().put(MIN_EXTENT_KEY, null);
        modelBlackboard().put(MAX_EXTENT_KEY, null);
        //It's expensive to recompute these cached values, so queue the call
        //to #updateComponents [Jon Aquino]
        queueComponentUpdate();
    }

    private LineSegmentEnvelopeIntersector lineSegmentEnvelopeIntersector =
        new LineSegmentEnvelopeIntersector();

    // Modified by [mmichaud 2007-05-26] to use decimation algorithm from Java2DConverter
    private Shape getWireFrame() throws NoninvertibleTransformException {
        // affineTransform computed according to the zoombar slider (getScale)
        affineTransform = Viewport.modelToViewTransform(
                getScale(),
                new Point2D.Double(
                    proposedModelEnvelope().getMinX(), proposedModelEnvelope().getMinY()),
                layerViewPanel().getSize().getHeight());
        // view2D rectangle
        Rectangle2D view2D = new Rectangle2D.Double(
            0.0, 0.0, layerViewPanel().getWidth(), layerViewPanel().getWidth());
        GeneralPath wireFrame = new GeneralPath();
        ArrayList segments = new ArrayList(getSegmentCache());
        //segments.addAll(toSegments(randomOnScreenGeometries()));
        segments.addAll(toSegments(largeOnScreenGeometries()));
        for (Iterator i = segments.iterator(); i.hasNext();) {
            Coordinate[] coordinates =
                java2DConverter.toViewCoordinates((Coordinate[]) i.next());
            boolean drawing = false;
            
            for (int j = 1; j < coordinates.length; j++) {
                if (!view2D.intersectsLine(coordinates[j - 1].x, coordinates[j - 1].y,
                                           coordinates[j].x, coordinates[j].y)) {
                    drawing = false;
                    continue;
                }
                if (!drawing) {
                    wireFrame.moveTo((float) coordinates[j - 1].x,
                                     (float) coordinates[j - 1].y);
                }
                wireFrame.lineTo((float) coordinates[j].x,
                                 (float) coordinates[j].y);
                drawing = true;
            }
            
        }
        return wireFrame;
        
    }

    private Collection getSegmentCache() throws NoninvertibleTransformException {
        //Use LayerManager blackboard for segment cache, so that multiple
        //views can share it. [Jon Aquino]
        if (modelBlackboard().get(SEGMENT_CACHE_KEY) == null) {
            //modelBlackboard().put(SEGMENT_CACHE_KEY, toSegments(randomGeometries()));
            modelBlackboard().put(SEGMENT_CACHE_KEY, toSegments(largeGeometries()));
            //
            // We only want to do this when a frame closes. If we don't clear the
            // cache in the blackboard then we'll get a memory leak.
            //
            frame.getActiveInternalFrame().addInternalFrameListener(
                    new InternalFrameAdapter() {
                public void internalFrameClosing(InternalFrameEvent e) {
                    modelBlackboard().put(SEGMENT_CACHE_KEY, null);
                }
            });
        }
        return (Collection) modelBlackboard().get(SEGMENT_CACHE_KEY);
    }

    private Collection toSegments(Collection geometries) {
        ArrayList segments = new ArrayList();
        for (Iterator i = geometries.iterator(); i.hasNext();) {
            Geometry geometry = (Geometry) i.next();
            segments.addAll(CoordinateArrays.toCoordinateArrays(geometry, false));
        }
        return segments;
    }

    // Replace RANDOM_ONSCREEN_GEOMETRIES by LARGE_ONSCREEN_GEOMETRIES
    // private static final int RANDOM_ONSCREEN_GEOMETRIES = 100;
    // private static final int RANDOM_GEOMETRIES = 100;
    private static final int LARGE_GEOMETRIES = 100;
    private static final int LARGE_ONSCREEN_GEOMETRIES = 200;

    
    
    
    // start [mmichaud]
    // additional code to replace randomGeometries by a largestGeometries approach
    // one bad side effect of random geometries approach was visible for a big set of points
    // and a small set of polygons : polygons were not visible because of the proportional
    // selection of geometries, and points are not displayed :-(
    
    static final Comparator MAX_SIZE_COMPARATOR = new Comparator() {
        public int compare(Object f1, Object f2) {
            Envelope env1 = ((Feature)f1).getGeometry().getEnvelopeInternal();
            Envelope env2 = ((Feature)f2).getGeometry().getEnvelopeInternal();
            double size1 = Math.max(env1.getWidth(), env1.getHeight());
            double size2 = Math.max(env2.getWidth(), env2.getHeight());
            return size1 < size2 ? 1 : (size1 > size2 ? -1 : 0);
        }
    };
    
    private Collection largeGeometries(int maxSize, List features) {
        Collections.sort(features, MAX_SIZE_COMPARATOR);
        List geometries = new ArrayList();
        for (int i = 0 , max = Math.min(maxSize, features.size()) ; i < max ; i++) {
            geometries.add(((Feature)features.get(i)).getGeometry());
        }
        //System.out.println("" + geometries.size() + "/" + features.size());
        return geometries;
    }
    
    private Collection largeOnScreenGeometries() {
        List onScreenFeatures = new ArrayList();
        if (totalGeometries() == 0) {
            return onScreenFeatures;
        }
        // Use proposedModelEnvelope (dynamically computed while the mouse is dragged)
        // instead of layerViewPanel().getViewport().getEnvelopeInModelCoordinates()
        // [mmichaud 2007-05-27]
        Envelope modelEnvelope;
        try {
            modelEnvelope = proposedModelEnvelope();
        }
        catch(NoninvertibleTransformException e) {
            modelEnvelope = layerViewPanel().getViewport().getEnvelopeInModelCoordinates();
        }
        // Restrict to visible layers [mmichaud 2007-05-27]
        for (Iterator it = layerViewPanel().getLayerManager()
                                           .getVisibleLayers(true)
                                           .iterator() ;  it.hasNext() ; ) {
            Layer layer = (Layer) it.next();
            // Select features intersecting the window
            List visibleFeatures = layer.getFeatureCollectionWrapper().query(modelEnvelope);
            if (visibleFeatures.size() < 1000) {
                onScreenFeatures.addAll(layer.getFeatureCollectionWrapper().query(modelEnvelope));
            }
            // If there are more than 1000 visible features in this layer
            // select a maximum of 2000 features stepping through the list
            else {
                int step = visibleFeatures.size()/1000;
                for (int i = 0 , max = visibleFeatures.size() ; i < max ; i += step) {
                    onScreenFeatures.add(visibleFeatures.get(i));
                }
            }
        }
        return largeGeometries(LARGE_ONSCREEN_GEOMETRIES, onScreenFeatures);
    }
    
    private Collection largeGeometries() {
        ArrayList largeGeometries = new ArrayList();
        if (totalGeometries() == 0) {
            return largeGeometries;
        }
        Envelope modelEnvelope = layerViewPanel().getViewport().getEnvelopeInModelCoordinates();
        for (Iterator i = layerViewPanel().getLayerManager().getVisibleLayers(true).iterator(); i.hasNext();) {
            Layer layer = (Layer) i.next();
            List visibleFeatures = layer.getFeatureCollectionWrapper().query(modelEnvelope);
            largeGeometries.addAll(layer.getFeatureCollectionWrapper().getFeatures());
        }
        return largeGeometries(LARGE_GEOMETRIES, largeGeometries);
    }
    // end [mmichaud]
    
    // Replaced Random geometries by large geometries
    /*
    private Collection randomGeometries(int maxSize, List features) {
        if (features.size() <= maxSize) {
            return FeatureUtil.toGeometries(features);
        }
        ArrayList randomGeometries = new ArrayList();
        for (int j = 0; j < maxSize; j++) {
            randomGeometries.add(
                ((Feature) features.get((int) (Math.random() * features.size()))).getGeometry());
        }
        return randomGeometries;
    }
    
    private Collection randomOnScreenGeometries() {
        ArrayList randomOnScreenGeometries = new ArrayList();
        // Avoid method computation inside the loop [mmichaud]
        int totalGeometries = totalGeometries();
        if (totalGeometries == 0) {
            return randomOnScreenGeometries;
        }
        // Use proposedModelEnvelope (dynamically computed while the mouse is dragged)
        // instead of layerViewPanel().getViewport().getEnvelopeInModelCoordinates()
        // [mmichaud 2007-05-27]
        Envelope modelEnvelope;
        try {
            modelEnvelope = proposedModelEnvelope();
        }
        catch(NoninvertibleTransformException e) {
            modelEnvelope = layerViewPanel().getViewport().getEnvelopeInModelCoordinates();
        }
        // Restrict to visible layers [mmichaud 2007-05-27]
        for (Iterator i = layerViewPanel().getLayerManager().getVisibleLayers(true).iterator(); i.hasNext();) {
            Layer layer = (Layer) i.next();
            randomOnScreenGeometries.addAll(
                randomGeometries(
                    RANDOM_ONSCREEN_GEOMETRIES
                        * layer.getFeatureCollectionWrapper().size()
                        / totalGeometries(),
                    layer.getFeatureCollectionWrapper().query(
                        //layerViewPanel().getViewport().getEnvelopeInModelCoordinates())));
                        modelEnvelope)));
        }
        return randomOnScreenGeometries;
    }
    
    private Collection randomGeometries() {
        ArrayList randomGeometries = new ArrayList();
        if (totalGeometries() == 0) {
            return randomGeometries;
        }
        // Restrict to visible layers [mmichaud 2007-05-27]
        for (Iterator i = layerViewPanel().getLayerManager().getVisibleLayers(true).iterator(); i.hasNext();) {
            Layer layer = (Layer) i.next();
            randomGeometries.addAll(
                randomGeometries(
                    RANDOM_GEOMETRIES
                        * layer.getFeatureCollectionWrapper().size()
                        / totalGeometries(),
                    layer.getFeatureCollectionWrapper().getFeatures()));

        }
        return randomGeometries;
    }
    */

    private int toSliderValue(double scale) throws NoninvertibleTransformException {
        return slider.getMaximum()
            - (int) (slider.getMaximum()
                * (MathUtil.base10Log(scale) - MathUtil.base10Log(getMinScale()))
                / (MathUtil.base10Log(getMaxScale()) - MathUtil.base10Log(getMinScale())));
    }
    private double getMinExtent() throws NoninvertibleTransformException {
        if (modelBlackboard().get(MIN_EXTENT_KEY) == null) {
            double smallSegmentLength = chooseSmallSegmentLength(getSegmentCache());
            //-1 smallSegmentLength means there is no data or the data are all
            //points (i.e. no segments). [Jon Aquino]
            if (smallSegmentLength == -1) {
                return -1;
            }
            modelBlackboard().put(MIN_EXTENT_KEY, smallSegmentLength);
        }
        Assert.isTrue(modelBlackboard().getDouble(MIN_EXTENT_KEY) > 0);
        return modelBlackboard().getDouble(MIN_EXTENT_KEY);
    }

    private double chooseSmallSegmentLength(Collection segmentCache) {
        int segmentsChecked = 0;
        double smallSegmentLength = -1;
        for (Iterator i = segmentCache.iterator(); i.hasNext();) {
            Coordinate[] coordinates = (Coordinate[]) i.next();
            for (int j = 1; j < coordinates.length; j++) {
                double segmentLength = coordinates[j].distance(coordinates[j - 1]);
                segmentsChecked++;
                if (segmentLength > 0
                    && (smallSegmentLength == -1 || segmentLength < smallSegmentLength)) {
                    smallSegmentLength = segmentLength;
                }
                if (segmentsChecked > 100) {
                    break;
                }
            }
            if (segmentsChecked > 100) {
                break;
            }
        }
        return smallSegmentLength;
    }

    private double getMaxExtent() throws NoninvertibleTransformException {
        if (modelBlackboard().get(MAX_EXTENT_KEY) == null) {
            if (getSegmentCache().isEmpty()) {
                return -1;
            }
            modelBlackboard().put(
                MAX_EXTENT_KEY,
                layerViewPanel().getLayerManager().getEnvelopeOfAllLayers().getWidth());
        }
        return modelBlackboard().getDouble(MAX_EXTENT_KEY);
    }

    private double getMaxScale() throws NoninvertibleTransformException {
        double maxScale =
            (getMinExtent() == -1 || getMinExtent() == 0)
                ? 1E3
                : (1000 * layerViewPanel().getWidth() / getMinExtent());
        if (viewBlackboard().get(USER_DEFINED_MAX_SCALE) != null) {
            return Math.max(maxScale, viewBlackboard().getDouble(USER_DEFINED_MAX_SCALE));
        }
        return maxScale;
    }

    private double getMinScale() throws NoninvertibleTransformException {
        double minScale =
            (getMaxExtent() == -1 || getMaxExtent() == 0)
                ? 1E-3
                : (0.001 * layerViewPanel().getWidth() / getMaxExtent());
        if (viewBlackboard().get(USER_DEFINED_MIN_SCALE) != null) {
            return Math.min(minScale, viewBlackboard().getDouble(USER_DEFINED_MIN_SCALE));
        }
        return minScale;
    }

    private double toScale(int sliderValue) throws NoninvertibleTransformException {
        return Math.pow(
            10,
            ((slider.getMaximum() - sliderValue)
                * (MathUtil.base10Log(getMaxScale()) - MathUtil.base10Log(getMinScale()))
                / slider.getMaximum())
                + MathUtil.base10Log(getMinScale()));
    }
    private void setComponentsEnabled(boolean componentsEnabled) {
        slider.setEnabled(componentsEnabled);
        label.setEnabled(componentsEnabled);
    }
    private static final String SCALE_KEY = ZoomBar.class.getName() + " - SCALE";
    private static final String CENTRE_KEY = ZoomBar.class.getName() + " - CENTRE";
    //Store centre-locked flag on blackboard rather than field because there could
    //be several zoom bars [Jon Aquino]
    private static final String CENTRE_LOCKED_KEY = ZoomBar.class.getName() + " - CENTRE LOCKED";
    private static final String MIN_EXTENT_KEY = ZoomBar.class.getName() + " - MIN EXTENT";
    private static final String USER_DEFINED_MIN_SCALE =
        ZoomBar.class.getName() + " - USER DEFINED MIN SCALE";
    private static final String USER_DEFINED_MAX_SCALE =
        ZoomBar.class.getName() + " - USER DEFINED MAX SCALE";
    private static final String MAX_EXTENT_KEY = ZoomBar.class.getName() + " - MAX EXTENT";

    private Blackboard viewBlackboard() {
        return layerViewPanel() != null ? layerViewPanel().getBlackboard() : new Blackboard();
    }
    private Blackboard modelBlackboard() {
        return layerViewPanel().getLayerManager().getBlackboard();
    }
    private final LayerViewPanel dummyLayerViewPanel = new LayerViewPanel(new LayerManager(), new LayerViewPanelContext() {

		public void setStatusMessage(String message) {
		}

		public void warnUser(String warning) {
		}

		public void handleThrowable(Throwable t) {
		}
		
    });

    private LayerViewPanel layerViewPanel() {
        if (!(frame.getActiveInternalFrame() instanceof LayerViewPanelProxy)) {
            return dummyLayerViewPanel;
        }
        return ((LayerViewPanelProxy) frame.getActiveInternalFrame()).getLayerViewPanel();
    }
    void jbInit() throws Exception {
        this.setLayout(borderLayout1);
        label.setText(" ");
        slider.setPaintLabels(true);
        slider.setToolTipText(I18N.get("ui.zoom.ZoomBar.zoom"));
        slider.setMaximum(1000);
        this.add(slider, BorderLayout.CENTER);
        this.add(label, BorderLayout.EAST);
    }

    private void updateLabel() throws NoninvertibleTransformException {
        //Inexpensive. [Jon Aquino]
        label.setText(chooseGoodIncrement(getScale()).toString());
    }

    private RoundQuantity chooseGoodIncrement(double scale) {
        return incrementChooser.chooseGoodIncrement(
            metricUnits,
            layerViewPanel().getWidth() / scale);
    }

    private Font sliderLabelFont = new Font("Dialog", Font.PLAIN, 10);
    private boolean showingSliderLabels;
    private void updateSliderLabels() throws NoninvertibleTransformException {
        //Expensive if the data cache has been cleared. [Jon Aquino]
        if (!showingSliderLabels) {
            return;
        }
        if (!(slider.getUI() instanceof BasicSliderUI)) {
            return;
        }
        Hashtable labelTable = new Hashtable();
        final int LABEL_WIDTH = 60;
        int lastLabelPosition = -2 * LABEL_WIDTH;
        for (int i = 0; i < slider.getWidth(); i++) {
            if (i < (lastLabelPosition + LABEL_WIDTH)) {
                continue;
            }
            int sliderValue = ((BasicSliderUI) slider.getUI()).valueForXPosition(i);
            JLabel label = new JLabel(chooseGoodIncrement(toScale(sliderValue)).toString());
            label.setFont(sliderLabelFont);
            labelTable.put(new Integer(sliderValue), label);
            lastLabelPosition = i;
        }
        if (labelTable.isEmpty()) {
            //Get here during initialization. [Jon Aquino]
            return;
        }
        slider.setLabelTable(labelTable);
    }
    
   /**
    * Return a Point2D in the view model from a model coordinate.
    */
    // Added to implement Java2DConverter.PointConverter interface (used in getWireFrame)
    // [mmichaud 2007-05-27]
    public Point2D toViewPoint(Coordinate modelCoordinate)
        throws NoninvertibleTransformException {
        Point2D.Double pt = new Point2D.Double(modelCoordinate.x, modelCoordinate.y);
        return affineTransform.transform(pt, pt);
    }
    
}
